package com.zeyu.framework.core.web.utils.tag;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.util.Map;

/**
 * beetl 使用自定义函数写了shiro tag功能，你可以像使用jsp的shiro标签那样使用shiro
 * 先注册: gt.registerFunctionPackage("shiro",new ShiroExt ()); 或者配置文件注册
 * 你可以在模板里直接调用，譬如
 * <!--:
 * if(shiro.isGuest()) {}
 * -->
 * Created by zeyuphoenix on 2016/3/23.
 */
@Service
public class ShiroExt {

    // ================================================================
    // Constants
    // ================================================================

    /**
     * Logger
     */
    private static final Logger log = LoggerFactory.getLogger(ShiroExt.class);

    /**
     * 分隔符
     */
    private static final String PERMISSION_NAMES_DELIMETER = ",";

    // ================================================================
    // Fields
    // ================================================================

    // ================================================================
    // Constructors
    // ================================================================

    // ================================================================
    // Methods from/for super Interfaces or SuperClass
    // ================================================================

    // ================================================================
    // Public or Protected Methods
    // ================================================================

    /**
     * Displays body content only if the current Subject IS NOT known to the system, either
     * because they have not logged in or they have no corresponding 'RememberMe' identity. It is logically
     * opposite to the 'user' tag.
     *
     * @return guest or not
     */
    public boolean isGuest() {

        if (getSubject() == null || getSubject().getPrincipal() == null) {
            if (log.isTraceEnabled()) {
                log.trace("Subject does not exist or does not have a known identity (aka 'principal').  " +
                        "Tag body will be evaluated.");
            }
            return true;
        } else {
            if (log.isTraceEnabled()) {
                log.trace("Subject exists or has a known identity (aka 'principal').  " +
                        "Tag body will not be evaluated.");
            }
            return false;
        }
    }

    /**
     * Displays body content only if the current Subject has a known identity, either
     * from a previous login or from 'RememberMe' services. Note that this is semantically different
     * from the 'authenticated' tag, which is more restrictive. It is logically
     * opposite to the 'guest' tag.
     *
     * @return user or not
     */
    public boolean isUser() {
        if (getSubject() != null && getSubject().getPrincipal() != null) {
            if (log.isTraceEnabled()) {
                log.trace("Subject has known identity (aka 'principal').  " +
                        "Tag body will be evaluated.");
            }
            return true;
        } else {
            if (log.isTraceEnabled()) {
                log.trace("Subject does not exist or have a known identity (aka 'principal').  " +
                        "Tag body will not be evaluated.");
            }
            return false;
        }
    }

    /**
     * Displays body content only if the current user has successfully authenticated
     * _during their current session_. It is more restrictive than the 'user' tag.
     * It is logically opposite to the 'notAuthenticated' tag.
     *
     * @return auth or not.
     */
    public boolean isAuthenticated() {

        if (getSubject() != null && getSubject().isAuthenticated()) {
            if (log.isTraceEnabled()) {
                log.trace("Subject exists and is authenticated.  Tag body will be evaluated.");
            }
            return true;
        } else {
            if (log.isTraceEnabled()) {
                log.trace("Subject does not exist or is not authenticated.  Tag body will not be evaluated.");
            }
            return false;
        }
    }

    /**
     * Displays body content only if the current user has NOT succesfully authenticated
     * _during their current session_. It is logically opposite to the 'authenticated' tag.
     *
     * @return not auth or auth.
     */
    public boolean isNotAuthenticated() {
        if (getSubject() == null || !getSubject().isAuthenticated()) {
            if (log.isTraceEnabled()) {
                log.trace("Subject does not exist or is not authenticated.  Tag body will be evaluated.");
            }
            return true;
        } else {
            if (log.isTraceEnabled()) {
                log.trace("Subject exists and is authenticated.  Tag body will not be evaluated.");
            }
            return false;
        }
    }

    /**
     * Displays the user's principal or a property of the user's principal.
     *
     * @param map {String type, String property, String defaultValue}
     * @return value
     */
    public String principal(Map<String, String> map) {
        String strValue = null;

        if (getSubject() != null) {
            // Get the principal to print out
            Object principal;
            String type = map != null ? map.get("type") : null;
            if (type == null) {
                principal = getSubject().getPrincipal();
            } else {
                principal = getPrincipalFromClassName(type);
            }
            String property = map != null ? map.get("property") : null;
            // Get the string value of the principal
            if (principal != null) {
                if (property == null) {
                    strValue = principal.toString();
                } else {
                    strValue = getPrincipalProperty(principal, property);
                }
            }

        }

        if (strValue != null) {
            return strValue;
        } else {
            return map != null ? map.get("defaultValue") : null;
        }
    }

    /**
     * Displays body content only if the current user has the specified role.
     *
     * @param roleName role name.
     * @return has role or not.
     */
    public boolean hasRole(String roleName) {
        return getSubject() != null && getSubject().hasRole(roleName);
    }

    /**
     * Displays body content only if the current user does NOT have the specified role
     * (i.e. they explicitly lack the specified role)
     *
     * @param roleName role name.
     * @return lack role or not.
     */
    public boolean lacksRole(String roleName) {
        boolean hasRole = getSubject() != null
                && getSubject().hasRole(roleName);
        return !hasRole;
    }

    /**
     * Displays body content only if the current user has one of the specified roles from a
     * comma-separated list of role names.
     *
     * @param roleNames role name.
     * @return has role in its.
     */
    public boolean hasAnyRole(String roleNames) {
        boolean hasAnyRole = false;

        Subject subject = getSubject();
        if (subject != null) {

            // Iterate through roles and check to see if the user has one of the
            // roles
            for (String role : roleNames.split(",")) {

                if (subject.hasRole(role.trim())) {
                    hasAnyRole = true;
                    break;
                }
            }
        }

        return hasAnyRole;
    }

    /**
     * Displays body content only if the current Subject (user)
     * 'has' (implies) the specified permission (i.e the user has the specified ability).
     *
     * @param p permission.
     * @return has permission or not.
     */
    public boolean hasPermission(String p) {
        return getSubject() != null && getSubject().isPermitted(p);
    }

    /**
     * Displays body content only if the current Subject (user) does
     * NOT have (not imply) the specified permission (i.e. the user lacks the specified ability)
     *
     * @param p permission.
     * @return lack permission or not.
     */
    public boolean lacksPermission(String p) {
        return !hasPermission(p);
    }

    /**
     * Displays body content only if the current user has one of the specified permissions from a
     * comma-separated list of permission names.
     *
     * @param permissionNames permission name list, by ',' split
     * @return has any permission or not.
     */
    public boolean hasAnyPermissions(String permissionNames) {
        boolean hasAnyPermission = false;

        Subject subject = getSubject();
        if (subject != null) {
            // Iterate through permissions and check to see if the user has one of the permissions
            for (String permission : permissionNames.split(PERMISSION_NAMES_DELIMETER)) {

                if (subject.isPermitted(permission.trim())) {
                    hasAnyPermission = true;
                    break;
                }
            }
        }
        return hasAnyPermission;
    }

    /**
     * 获取系统shiro权限
     *
     * @return 权限主体
     */
    protected Subject getSubject() {
        return SecurityUtils.getSubject();
    }

    // ================================================================
    // Getter & Setter
    // ================================================================

    // ================================================================
    // Private Methods
    // ================================================================

    /*
     *get principal by class name.
     */
    private Object getPrincipalFromClassName(String type) {
        Object principal = null;

        try {
            Class<?> cls = Class.forName(type);
            principal = getSubject().getPrincipals().oneByType(cls);
        } catch (ClassNotFoundException e) {
            log.error("not principal class found.");
        }
        return principal;
    }

    /*
     * get principal property
     */
    private String getPrincipalProperty(Object principal, String property) {
        String strValue = null;

        try {
            BeanInfo bi = Introspector.getBeanInfo(principal.getClass());

            // Loop through the properties to get the string value of the
            // specified property
            boolean foundProperty = false;
            for (PropertyDescriptor pd : bi.getPropertyDescriptors()) {
                if (pd.getName().equals(property)) {
                    Object value = pd.getReadMethod().invoke(principal,
                            (Object[]) null);
                    strValue = String.valueOf(value);
                    foundProperty = true;
                    break;
                }
            }

            if (!foundProperty) {
                final String message = "Property [" + property
                        + "] not found in principal of type ["
                        + principal.getClass().getName() + "]";

                throw new RuntimeException(message);
            }

        } catch (Exception e) {
            final String message = "Error reading property [" + property
                    + "] from principal of type ["
                    + principal.getClass().getName() + "]";

            throw new RuntimeException(message, e);
        }

        return strValue;
    }

    // ================================================================
    // Inner or Anonymous Class
    // ================================================================

    // ================================================================
    // Test Methods
    // ================================================================
}
